const fs = require('fs-extra');
const path = require('path');
const randomString = require('randomstring');
// eslint-disable-next-line import/no-extraneous-dependencies
const canvas = require('canvas');
const utility = require('../../../utility');

const textFont = {
  'font-size': '12px',
  'font-family': 'Arial',
};
const fileRectbgColor = '#f1f1fb';
const functionRectbgColor = '#eaeaec';
const lineStyle = {
  width: 2,
  color: '#3269f6',
};
// 箭头颜色
const arrowFillColor = '#3269f6';
// 节点所在位置
const nodeArea = {
  left: 1,
  center: 2,
  right: 3,
};
// 节点列间隔距离
const nodeColMargin = 10;
// 节点行间隔
const nodeRowMargin = 10;
// label行距离
const nameRowHeight = 20;
// 每行字符个数
const nameLength = 22;
// 每个字符像素
const singleCharPx = 6;
// 线偏移量
const lineOffset = 9;
// 区域间隔
const areaColMargin = 60;
class CascoDrawFileInvokeFunc {
  constructor(data, output) {
    const fontPath = path.join(__dirname, '..', 'fonts', 'SourceHanSansCN-Normal.otf');
    canvas.registerFont(fontPath, { family: textFont['font-family'], style: textFont });
    this.data = data;
    this.output = output;
    this.nodesMap = new Map();
    // 区域间隔
    this.leftPosition = {
      x: 0, y: 10, width: 100, colCount: 30,
    };
    this.centerPosition = {
      x: 0, y: 10, width: 110, funcColCount: 10, funcWidth: 100, padding: 5,
    };
    this.rightPosition = {
      x: 0, y: 10, width: 100, colCount: 30,
    };
    this.svgConfig = {
      width: 0, height: 0, padding: 10, 'background-color': '#ffedea', maxWidth: 2000, maxHeight: 2000,
    };
    this.canvasCtx = null;
  }

  async generateImage() {
    this.nodeLayout(this.data.files);
    const width = this.svgConfig.width + this.svgConfig.padding;
    const height = this.svgConfig.height + this.svgConfig.padding;
    const imageCanvas = canvas.createCanvas(width, height);
    this.canvasCtx = imageCanvas.getContext('2d');
    // 左右文件
    ['left', 'right'].forEach((key) => {
      for (let i = 0; i < this.data.files[key].length; i += 1) {
        const file = this.data.files[key][i];
        this.drawFiles(file);
      }
    });
    // 中
    this.drawFiles(this.data.files.center);
    // 画线
    if (this.data.edges) {
      for (let i = 0; i < this.data.edges.length; i += 1) {
        this.drawEdges(this.data.edges[i]);
      }
    }
    const imagePath = path.join(this.output, `${randomString.generate(16)}.png`);
    await fs.writeFile(imagePath, imageCanvas.toBuffer());
    return { imagePath, height, width };
  }

  /**
 * 计算文本位置
 * @param {String} str 字符
 * @param {Numner} size 长度
 * @returns
 */
  textLength(str, size, x = 0, y = 0, parentwidth) {
    if (!size) {
      const strLen = str.length;
      const strPx = singleCharPx * strLen;
      size = strPx < parentwidth ? strLen : strLen - Math.ceil((strPx - parentwidth) / singleCharPx);
    }
    const strArray = [];
    for (let i = 0; i <= str.length; i += size) {
      const len = i + size;
      if (len <= str.length) {
        strArray.push(str.substring(i, len));
      } else if (i !== str.length) {
        strArray.push(str.substring(str.length, i));
      }
    }
    const positionArray = [];
    for (let i = 0; i < strArray.length; i += 1) {
      const newX = x + Math.floor((parentwidth - strArray[i].length * singleCharPx) / 2);
      positionArray.push({
        name: strArray[i],
        x: newX,
        y,
      });
      y += nameRowHeight;
    }
    return {
      positionArray, x, y, height: nameRowHeight * positionArray.length,
    };
  }

  drawArrowhead(x, y, direction = 'right') {
    this.canvasCtx.save();
    this.canvasCtx.beginPath();
    // 重新设置起始点
    this.canvasCtx.translate(x, y);
    const arrowOffsetX = 8;
    const arrowOffsetY = 3;
    this.canvasCtx.moveTo(0, 0);
    if (direction === 'right') {
      this.canvasCtx.lineTo(0, arrowOffsetY);
      this.canvasCtx.lineTo(arrowOffsetX, 0);
      this.canvasCtx.lineTo(0, -arrowOffsetY);
      this.canvasCtx.lineTo(0, 0);
    } else {
      this.canvasCtx.lineTo(0, arrowOffsetY);
      this.canvasCtx.lineTo(-arrowOffsetX, 0);
      this.canvasCtx.lineTo(0, -arrowOffsetY);
      this.canvasCtx.lineTo(0, 0);
    }
    this.canvasCtx.closePath();
    this.canvasCtx.restore();
    this.canvasCtx.fillStyle = arrowFillColor;
    this.canvasCtx.fill();
  }

  /**
 * 三次贝塞尔曲线
 * @param {*} begin
 * @param {*} control
 * @param {*} end
 */
  drawThreeOrderBezierLine(begin, end, direction) {
    const xOffset = Math.abs(end.x - begin.x) / 2;
    const centerX = end.x < begin.x ? end.x + xOffset : end.x - xOffset;
    this.canvasCtx.restore();
    this.canvasCtx.beginPath();
    this.canvasCtx.moveTo(begin.x, begin.y);
    this.canvasCtx.bezierCurveTo(centerX, begin.y, centerX, end.y, end.x, end.y);
    this.canvasCtx.strokeStyle = lineStyle.color;
    this.canvasCtx.lineWidth = lineStyle.width;
    this.canvasCtx.stroke();
    this.canvasCtx.save();
    this.drawArrowhead(end.x, end.y, direction);
  }

  /**
 * 二次贝塞尔曲线
 * @param {*} begin
 * @param {*} control
 * @param {*} end
 */
  drawTwoOrderBezierLine(begin, control, end, direction) {
    this.canvasCtx.restore();
    this.canvasCtx.beginPath();
    this.canvasCtx.moveTo(begin.x, begin.y);
    this.canvasCtx.quadraticCurveTo(control.x, control.y, end.x, end.y);
    this.canvasCtx.strokeStyle = lineStyle.color;
    this.canvasCtx.lineWidth = lineStyle.width;
    this.canvasCtx.stroke();
    this.canvasCtx.save();
    this.drawArrowhead(end.x, end.y, direction);
  }

  drawEdges(lineData) {
    if (this.nodesMap.size === 0
      || !lineData.from
      || !lineData.to
      || !this.nodesMap.get(lineData.from)
      || !this.nodesMap.get(lineData.to)) {
      return;
    }
    const startNode = this.nodesMap.get(lineData.from);
    const endNode = this.nodesMap.get(lineData.to);
    let lineStartX = startNode.x;
    let lineStartY = startNode.y;
    let lineEndX = endNode.x;
    let lineEndY = endNode.y;
    let direction;
    // 线补充
    // 重新计算线开始和结束节点位置
    if (startNode.position === nodeArea.center) {
      switch (endNode.position) {
        // center--->left
        case nodeArea.left: {
          lineStartY += Math.round(startNode.height / 2);
          lineEndX += endNode.width + lineOffset;
          lineEndY += Math.round(endNode.height / 2);
          direction = 'left';
          break;
        }
        // center--->right
        case nodeArea.right: {
          lineStartY += Math.round(startNode.height / 2);
          lineStartX += startNode.width;
          lineEndY += Math.round(endNode.height / 2);
          lineEndX -= lineOffset;
          direction = 'right';
          break;
        }
        // center--->center
        case nodeArea.center: {
          // 自己指向自己去掉线
          if (startNode.id === endNode.id) {
            break;
          }
          if (startNode.nodeIndex % 2) {
            // 奇数
            lineStartX += startNode.width;
            lineEndX += endNode.width + lineOffset;
            lineEndY += Math.round(endNode.height / 2);
            direction = 'left';
          } else {
            // 偶数
            lineEndY += Math.round(endNode.height / 2);
            lineEndX -= lineOffset;
            direction = 'right';
          }
          break;
        }
        default: break;
      }
    }
    if (startNode.position === nodeArea.left) {
      lineStartX += startNode.width;
      lineStartY += Math.round(startNode.height / 2);
      lineEndY += Math.round(endNode.height / 2);
      lineEndX -= lineOffset;
      direction = 'right';
    }
    if (startNode.position === nodeArea.right) {
      lineStartY += Math.round(startNode.height / 2);
      lineEndX += endNode.width - lineOffset;
      lineEndY += Math.round(endNode.height / 2);
      direction = 'left';
    }
    if (startNode.position === nodeArea.center && endNode.position === nodeArea.center) {
      let controlX;
      let controlY;
      const rx = Math.abs(startNode.y - endNode.y) / 2;
      const xOffset = rx > startNode.width ? rx : startNode.width;
      if (startNode.nodeIndex % 2) {
        // 奇数
        controlX = lineStartX + xOffset;
        controlY = startNode.y;
      } else {
        // 偶数
        controlX = startNode.x - xOffset;
        controlY = startNode.y;
      }
      this.drawTwoOrderBezierLine(
        { x: lineStartX, y: lineStartY },
        { x: controlX, y: controlY },
        { x: lineEndX, y: lineEndY },
        direction
      );
    } else {
      this.drawThreeOrderBezierLine(
        { x: lineStartX, y: lineStartY },
        { x: lineEndX, y: lineEndY },
        direction
      );
    }
  }

  /**
 *  计算布局
 * @param {Object} data 数据
 */
  nodeLayout(data) {
    let leftAreaWidth = 0;
    if (data.left && data.left.length > 0) {
      const count = Math.ceil(data.left.length / this.leftPosition.colCount);
      leftAreaWidth = count * this.leftPosition.width + (count - 1) * nodeColMargin;
      this.centerPosition.x = leftAreaWidth + areaColMargin;
    } else {
      this.centerPosition.x += areaColMargin;
    }
    // 计算左边文件布局
    const leftFiles = utility.arrayChunk(data.left, this.leftPosition.colCount);
    this.svgConfig.width = leftAreaWidth;
    const leftOriginY = this.leftPosition.y;
    for (let i = 0; i < leftFiles.length; i += 1) {
      const colNodes = leftFiles[i];
      // 每一列文件开始位置
      this.leftPosition.x += i * (this.leftPosition.width + nodeColMargin);
      this.leftPosition.y = leftOriginY;
      for (let j = 0; j < colNodes.length; j += 1) {
        const node = colNodes[j];
        node.x = this.leftPosition.x;
        node.y = this.leftPosition.y;
        node.width = this.leftPosition.width;
        const names = this.textLength(node.name, nameLength, this.leftPosition.x, this.leftPosition.y, this.leftPosition.width);
        node.height = names.height;
        node.names = names.positionArray;
        this.leftPosition.y = names.y + nodeRowMargin;
        node.position = nodeArea.left;
        node.nodeIndex = i;
        this.nodesMap.set(node.id, node);
      }
      // 重置画布大小
      if (this.leftPosition.y > this.svgConfig.height) {
        this.svgConfig.height = this.leftPosition.y;
      }
    }
    // 计算中间文件布局
    if (data.center) {
      const fileNode = data.center;
      const originY = this.centerPosition.y;
      fileNode.x = this.centerPosition.x;
      fileNode.y = this.centerPosition.y;
      fileNode.width = this.centerPosition.width;
      // 中心文件位置计算
      const functionCount = fileNode.functions.length;
      const colCount = Math.ceil(functionCount / this.centerPosition.funcColCount);
      const centerMaxWidth = colCount * this.centerPosition.width + (colCount - 1) * nodeColMargin;
      const fileNames = this.textLength(fileNode.name, null, this.centerPosition.x,
        this.centerPosition.y, centerMaxWidth - this.centerPosition.padding);
      fileNode.height = fileNames.height + this.centerPosition.padding;
      fileNode.names = fileNames.positionArray;
      if (functionCount > 0) {
        const funcsArray = utility.arrayChunk(fileNode.functions, this.centerPosition.funcColCount);
        let funcsMaxHeight = 0;
        for (let i = 0; i < funcsArray.length; i += 1) {
          const funcs = funcsArray[i];
          this.centerPosition.y = fileNames.y + nodeRowMargin;
          if (i > 0) {
            this.centerPosition.x += nodeColMargin;
            fileNode.width = fileNode.width + nodeColMargin + this.centerPosition.funcWidth;
          }
          let funcHeight = 0;
          for (let j = 0; j < funcs.length; j += 1) {
            const funcNode = funcs[j];
            funcNode.x = this.centerPosition.x + (this.centerPosition.width - this.centerPosition.funcWidth) / 2;
            funcNode.y = this.centerPosition.y;
            funcNode.width = this.centerPosition.funcWidth;
            const funcNames = this.textLength(funcNode.name, null, funcNode.x, this.centerPosition.y, funcNode.width);
            funcNode.height = funcNames.height;
            funcNode.names = funcNames.positionArray;
            this.centerPosition.y = this.centerPosition.y + funcNode.height + nodeRowMargin;
            funcHeight = funcHeight + funcNode.height + nodeRowMargin;
            funcNode.position = nodeArea.center;
            funcNode.nodeIndex = j;
            this.nodesMap.set(funcNode.id, funcNode);
          }
          this.centerPosition.x += this.centerPosition.funcWidth;
          if (funcsMaxHeight < funcHeight) {
            funcsMaxHeight = funcHeight;
          }
        }
        fileNode.height += funcsMaxHeight;
      }
      fileNode.position = nodeArea.center;
      fileNode.nodeIndex = 0;
      // 设置边界间距
      fileNode.x -= this.centerPosition.padding;
      fileNode.width += 2 * this.centerPosition.padding;

      // 重置画布大小
      if (originY + fileNode.height > this.svgConfig.height) {
        this.svgConfig.height = originY + fileNode.height;
      }
      this.svgConfig.width += fileNode.width + areaColMargin;
    }
    // 计算右边布局位置
    this.svgConfig.width += areaColMargin;
    if (data.right && data.right.length > 0) {
      this.rightPosition.x = this.svgConfig.width;
      const rightFiles = utility.arrayChunk(data.right, this.rightPosition.colCount);
      const rightOriginY = this.rightPosition.y;
      for (let i = 0; i < rightFiles.length; i += 1) {
        const colNodes = rightFiles[i];
        this.rightPosition.y = rightOriginY;
        // 每一列文件开始位置
        if (i > 0) {
          this.rightPosition.x += nodeColMargin;
        }
        for (let j = 0; j < colNodes.length; j += 1) {
          const node = colNodes[j];
          node.x = this.rightPosition.x;
          node.y = this.rightPosition.y;
          node.width = this.rightPosition.width;
          const names = this.textLength(node.name, null, this.rightPosition.x, this.rightPosition.y, node.width);
          node.height = names.height;
          node.names = names.positionArray;
          this.rightPosition.y = names.y + nodeRowMargin;
          node.position = nodeArea.right;
          node.nodeIndex = j;
          this.nodesMap.set(node.id, node);
        }
        this.rightPosition.x += this.rightPosition.width;
        // 重置画布高
        if (this.rightPosition.y > this.svgConfig.height) {
          this.svgConfig.height = this.rightPosition.y;
        }
      }
      // 重置画布宽
      this.svgConfig.width = this.rightPosition.x;
    }
  }

  drawRect(position, color) {
    this.canvasCtx.restore();
    this.canvasCtx.fillStyle = color;
    this.canvasCtx.fillRect(position.x, position.y, position.width, position.height);
    this.canvasCtx.save();
  }

  drawText(text, textFont) {
    this.canvasCtx.restore();
    this.canvasCtx.fillStyle = 'black';
    this.canvasCtx.font = `${textFont['font-size']} ${textFont['font-family']}`;
    this.canvasCtx.textAlign = 'start';
    this.canvasCtx.textBaseline = 'top';
    this.canvasCtx.fillText(text.name, text.x, text.y);
    this.canvasCtx.save();
  }

  drawFiles(file) {
    const funcs = file.functions || [];
    this.drawRect(file, fileRectbgColor);
    file.names.forEach((item) => {
      this.drawText(item, textFont);
    });
    funcs.forEach((func) => {
      this.drawRect(func, functionRectbgColor);
      func.names.forEach((item) => {
        this.drawText(item, textFont);
      });
    });
  }
}

module.exports = CascoDrawFileInvokeFunc;
